Imports System.Drawing
Imports System.Drawing.Drawing2D

Namespace Annotations

  ''' <summary>
  ''' Annotation consisting of a list of points, with proper events and the necessary methods 
  ''' for modifying those points.
  ''' </summary>
  ''' <remarks></remarks>
  Public Class Annotation
    Inherits Generic.List(Of PointF)
    Implements IDisposable, ICloneable, ISelectable

    Public Const NoPointSelected As Integer = -1
    Private Const MinSelectionSize As Integer = 5
    Private Const MinSelectionWidth As Integer = 3
    Private Const TextOffset As Integer = 7

    'Caching these should allow faster redraws. 
    Private needPathUpdate As Boolean = True, needRegionUpdate As Boolean = True

#Region "Events"
    ''' <summary>
    ''' Event that is triggered when an item or property changes.
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Public Event Changed(ByVal sender As Object, ByVal e As CollectionChangedEventArgs)

    ''' <summary>
    ''' Allows raising this event from derived classes.
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <remarks></remarks>
    Protected Friend Sub OnChangedEvent(ByVal sender As Object, ByVal e As CollectionChangedEventArgs)
      If e.Type <> CollectionChangedEventArgs.EventType.PropertySet Then
        Me.needPathUpdate = True
        Me.needRegionUpdate = True
      End If
      If Me._events Then RaiseEvent Changed(sender, e)
    End Sub

    Public Event SelectedChanged(ByVal sender As ISelectable) Implements ISelectable.SelectedChanged
    ''' <summary>
    ''' Allows raising this event from derived classes.
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <remarks></remarks>
    Protected Friend Sub OnSelectedChangedEvent(ByVal sender As ISelectable)
      If Me._events Then RaiseEvent SelectedChanged(sender)
    End Sub

#End Region

#Region "Constructor - Destructor"
    Public Sub New()
      MyBase.New(64)
      Debug.WriteLine("Annotation.New")
    End Sub

    Public Overridable Overloads Sub Dispose() Implements IDisposable.Dispose
      Dispose(True)
      GC.SuppressFinalize(Me)
    End Sub

    Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean)
      'We clean up, the program leaks memory otherwise due to unmanaged GDI+ objects
      Debug.WriteLine("Annotation.Dispose")
      If disposing Then
        ' Free other state (managed objects).
        MyBase.Clear()
      End If
    End Sub

    Protected Overrides Sub Finalize()
      ' Simply call Dispose(False).
      Dispose(False)
    End Sub

#End Region

#Region "Properties"
    Private _events As Boolean = True
    ''' <summary>
    ''' Enable or disable raising events. Default is true
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>Careful, setting this to false may speed up things but may also create mayhem when drawing.</remarks>
    Public Property Events() As Boolean
      Get
        Return Me._events
      End Get
      Set(ByVal value As Boolean)
        Me._events = value
      End Set
    End Property

    ''' <summary>
    ''' Get an item
    ''' </summary>
    ''' <param name="index"></param>
    ''' <value></value>
    ''' <remarks>Not very clear to use this in code, use 'Point' property instead. We cannot seem to hide this effectively ...
    ''' </remarks>
    Public Shadows Property Item(ByVal index As Integer) As PointF
      Get
        Return MyBase.Item(index)
      End Get
      Set(ByVal value As PointF)
        Me.Point(index) = value
      End Set
    End Property

    ''' <summary>
    ''' Get or set a point
    ''' </summary>
    ''' <param name="index"></param>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Point(ByVal index As Integer) As PointF
      Get
        Return MyBase.Item(index)
      End Get
      Set(ByVal value As PointF)
        MyBase.Item(index) = value
        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemAdded, value, index))
      End Set
    End Property

    Protected Friend _parent As AnnotationContainer
    ''' <summary>
    ''' The parent collection. Can be null.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public ReadOnly Property Parent() As AnnotationContainer
      Get
        Return _parent
      End Get
    End Property

    Friend _key As String
    ''' <summary>
    ''' The key by which the Annotation is known in its parent collection.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public ReadOnly Property Key() As String
      Get
        Return _key
      End Get
    End Property
#End Region

#Region "Draw properties"
    Private _region As Region
    ''' <summary>
    ''' A reference to the internally cached region corresponding to the Annotation, used in drawing.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>Never modify this object!
    ''' </remarks>
    Friend ReadOnly Property Region() As Region
      Get
        UpdateCachedRegion()
        Return _region
      End Get
    End Property

    Private _path As New GraphicsPath
    ''' <summary>
    ''' The cached readonly <b>GraphicsPath</b> of the Annotation, used for drawing, computing areas or doing hit testing.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>Never modify this object!
    ''' </remarks>
    Friend ReadOnly Property Path() As GraphicsPath
      Get
        UpdateCachedPath()
        Return _path
      End Get
    End Property

    Dim _selected As Boolean = False
    ''' <summary>
    ''' True if the Annotation is selected.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>Returns false if its parent container is selected!
    ''' </remarks>
    Public Property Selected() As Boolean Implements ISelectable.Selected
      Get
        Return Me._selected
      End Get
      Set(ByVal value As Boolean)
        Me._selected = value

        'Make sure no point is selected when deselecting annotation
        If Not value Then Me.SelectedPoint = Annotation.NoPointSelected
        Me.OnSelectedChangedEvent(Me)
      End Set
    End Property

    Private _selectedPoint As Integer = NoPointSelected
    ''' <summary>
    ''' The index of the selected point if any (-1 in that case).
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property SelectedPoint() As Integer
      Get
        Return _selectedPoint
      End Get
      Set(ByVal Value As Integer)
        _selectedPoint = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Private _AddMode As RegionAddMode = RegionAddMode.XorMode
    ''' <summary>
    ''' Determines how this annotations' region will be combined with the regions of the 
    ''' other annotations in the same Annotation container.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property AddMode() As RegionAddMode
      Get
        Return _AddMode
      End Get
      Set(ByVal Value As RegionAddMode)
        _AddMode = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Protected Friend _color As System.Drawing.Color = Drawing.Color.Empty
    ''' <summary>
    ''' The color used to draw the lines, points and text of the Annotation. 
    ''' </summary>
    ''' <value></value>
    ''' <remarks>If the color is left uninitialized it will be set to that of the annotation container when added to it. Note that 
    ''' selection of an annotation is not indicated by a different opacity as with the annotation containers, but by a wider pen.
    ''' </remarks>
    Public Property Color() As System.Drawing.Color
      Get
        Return _color
      End Get
      Set(ByVal Value As System.Drawing.Color)
        _color = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Private _visible As Boolean = True
    ''' <summary>
    ''' Determines if the Annotation will be drawn or not.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property Visible() As Boolean
      Get
        Return _visible
      End Get
      Set(ByVal Value As Boolean)
        _visible = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Private _enabled As Boolean = True
    ''' <summary>
    ''' Determines if the Annotation will interact with the user using the mouse.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property Enabled() As Boolean
      Get
        Return _enabled
      End Get
      Set(ByVal Value As Boolean)
        _enabled = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Private _text As String
    ''' <summary>
    ''' The text drawn near the first point of the Annotation
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property Text() As String
      Get
        Return _text
      End Get
      Set(ByVal Value As String)
        _text = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Private _closed As Boolean = True
    ''' <summary>
    ''' If true, the Annotation will be closed when drawn.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property Closed() As Boolean
      Get
        Return _closed
      End Get
      Set(ByVal Value As Boolean)
        _closed = Value
      End Set
    End Property

    Private _maximumNumberOfPointsOnCreation As Integer = -1
    ''' <summary>
    ''' If this value > -1 then interactive creation will automatically stop when the user 
    ''' has drawn the required number of points.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property MaximumNumberOfPointsOnCreation() As Integer
      'Maximum number of point to be added interactively
      Get
        Return _maximumNumberOfPointsOnCreation
      End Get
      Set(ByVal Value As Integer)
        _maximumNumberOfPointsOnCreation = Value
      End Set
    End Property

    Private _fontSize As Integer = 8
    ''' <summary>
    ''' The font size of the text.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property FontSize() As Integer
      'The size of the text
      Get
        Return _fontSize
      End Get
      Set(ByVal Value As Integer)
        _fontSize = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Protected Friend _pointSize As Integer = 3
    ''' <summary>
    ''' The size of the rectangles representing the Annotation points, in image coordinates.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property PointSize() As Integer
      'The size of the point marker
      Get
        Return _pointSize
      End Get
      Set(ByVal Value As Integer)
        _pointSize = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Protected Friend _lineWidth As Integer = 1
    ''' <summary>
    ''' The width of the lines connecting the Annotation points, in image coordinates
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property LineWidth() As Integer
      Get
        Return _lineWidth
      End Get
      Set(ByVal Value As Integer)
        _lineWidth = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

#End Region

#Region "Methods"

    ''' <summary>
    ''' Copy the properties to another annotation
    ''' </summary>
    ''' <param name="ann"></param>
    ''' <remarks></remarks>
    Public Sub CopyPropertiesTo(ByVal ann As Annotation)
      ann.Text = Me.Text
      ann.Color = Me.Color
      ann.Visible = Me.Visible
      ann.Enabled = Me.Enabled
      ann.AddMode = Me.AddMode
      ann.PointSize = Me.PointSize
      ann.LineWidth = Me.LineWidth
      ann.FontSize = Me.FontSize
      ann.MaximumNumberOfPointsOnCreation = Me.MaximumNumberOfPointsOnCreation
      ann.Closed = Me.Closed
    End Sub

    ''' <summary>
    ''' Copy the points to another annotation
    ''' </summary>
    ''' <param name="ann"></param>
    ''' <remarks>Only included in order to have the equivalent for CopyPropertiesTo</remarks>
    Public Sub CopyPointsTo(ByVal ann As Annotation)
      ann.AddRange(Me)
    End Sub

    ''' <summary>
    ''' Create a Annotation from an XML node.
    ''' </summary>
    ''' <param name="node"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function FromXML(ByVal node As Xml.XmlNode) As Annotation
      Dim ann As New Annotation
      Dim numberFormat As System.Globalization.NumberFormatInfo = System.Globalization.NumberFormatInfo.InvariantInfo

      'Title = text. Text visible if text node present
      ann.Text = Util.GetSVGText(node, "title")
      ann.FontSize = Util.GetSVGTextFontSize(node, "text")

      'Points visible if Marker is present
      Dim oMarkerNode As Xml.XmlNode = node.SelectSingleNode("./defs/Marker")
      If Not oMarkerNode Is Nothing Then
        ann.PointSize = Integer.Parse(oMarkerNode.Attributes.GetNamedItem("marker-height").Value)
      Else
        ann.PointSize = 0
      End If

      'Check if we have a polygon or polyline
      Dim oANode As Xml.XmlNode = node.SelectSingleNode("polyline")
      If oANode Is Nothing Then
        oANode = node.SelectSingleNode("polygon")
        ann.Closed = True
      Else
        ann.Closed = False
      End If

      If Not oANode Is Nothing Then
        Dim strPen As String = oANode.Attributes.GetNamedItem("stroke-width").Value
        Dim oAColor As Color = Util.WebColorToColor(oANode.Attributes.GetNamedItem("stroke").Value)

        ann.Color = oAColor
        ann.LineWidth = CInt(strPen)

        Dim oPtNode As Xml.XmlNode = oANode.Attributes.GetNamedItem("points")
        If Not oPtNode Is Nothing Then

          Dim strPts As String = oPtNode.Value
          'We need to parse this string to x y coordinates
          strPts = strPts.Trim()
          Dim strPt() As String = strPts.Split(CChar(","))
          If strPt.Length Mod 2 <> 0 Then
            'Should be even!!!!
            ReDim Preserve strPt(strPt.Length - 2)
          End If

          Dim i As Integer
          Dim pts(strPt.Length \ 2 - 1) As PointF
          For i = 0 To strPt.Length - 1 Step 2
            'Must be locale UNAWARE!!!!
            pts(i \ 2).X = Single.Parse(strPt(i), numberFormat)
            pts(i \ 2).Y = Single.Parse(strPt(i + 1), numberFormat)
          Next
          ann.AddRange(pts)
        End If
      End If

      Return ann
    End Function

    ''' <summary>
    ''' Serialize into a piece of XML.
    ''' </summary>
    ''' <param name="SVG"></param>
    ''' <remarks></remarks>
    Public Sub ToXML(ByVal SVG As Xml.XmlWriter)
      Dim numberFormat As System.Globalization.NumberFormatInfo = System.Globalization.NumberFormatInfo.InvariantInfo
      SVG.WriteStartElement("g")
      SVG.WriteAttributeString("id", Me.Key)

      'Marker
      Dim strMarkerID As String = "M" & Me.Key

      If Me.PointSize > 0 Then
        SVG.WriteStartElement("defs")
        SVG.WriteStartElement("Marker")
        SVG.WriteAttributeString("id", strMarkerID)
        SVG.WriteAttributeString("markerUnits", "strokeWidth")
        SVG.WriteAttributeString("orient", "auto")
        SVG.WriteAttributeString("marker-height", Me.PointSize.ToString)
        SVG.WriteAttributeString("marker-width", Me.PointSize.ToString)
        SVG.WriteStartElement("path")
        SVG.WriteAttributeString("d", "M 0 0 L 0 " & Me.PointSize & " " & Me.PointSize & " " & Me.PointSize & " " & Me.PointSize & " 0 z")
        SVG.WriteEndElement()
        SVG.WriteEndElement()
        SVG.WriteEndElement()
      End If

      'Add text in title. Render as text if possible
      If Not Me.Text Is Nothing Then
        SVG.WriteElementString("title", Me.Text)
        If Me.Count > 0 Then
          'Add text if present, at location of 1st point
          Util.AddSVGText(SVG, Me.Text, Me.Point(0), Me.FontSize, Me.Color, numberFormat)
        End If
      End If

      'Use a polyline or polygon, depending on the closed property
      If Me.Closed Then
        SVG.WriteStartElement("polygon")
      Else
        SVG.WriteStartElement("polyline")
      End If

      'Add stroke
      SVG.WriteAttributeString("stroke", Util.ColorToWebColor(Me.Color))
      SVG.WriteAttributeString("stroke-width", Me.LineWidth.ToString)

      'Add markers
      If Me.PointSize > 0 Then SVG.WriteAttributeString("marker", "url(#" & strMarkerID & ")")

      'Add the points
      Dim oPt As PointF, strPt As String
      strPt = ""
      For Each oPt In Me
        strPt = strPt & oPt.X.ToString(numberFormat) & "," & oPt.Y.ToString(numberFormat) & ","
      Next

      'Remove last comma
      If strPt.Length > 0 Then
        strPt = strPt.Substring(0, strPt.Length - 1)
        SVG.WriteAttributeString("points", strPt)
      End If
      SVG.WriteEndElement()

      'Close group
      SVG.WriteEndElement()
    End Sub

    ''' <summary>
    ''' Return a exact copy (same properties and points) of the Annotation.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overridable Function Clone() As Object Implements ICloneable.Clone
      Dim ann As New Annotation()
      Me.CopyPropertiesTo(ann)
      Me.CopyPointsTo(ann)
      Return ann
    End Function

    ''' <summary>
    ''' Add a point
    ''' </summary>
    ''' <param name="value"></param>
    ''' <remarks>
    ''' </remarks>
    Public Shadows Sub Add(ByVal value As PointF)
      MyBase.Add(value)
      OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemAdded, value, Count - 1))
    End Sub

    ''' <summary>
    ''' Add the points from an enumerable source of PointF
    ''' </summary>
    ''' <param name="value"></param>
    ''' <remarks>
    ''' </remarks>
    Public Shadows Sub AddRange(ByVal value As Generic.IEnumerable(Of PointF))
      MyBase.AddRange(value)
      OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsAdded, value, Count - 1))
    End Sub

    ''' <summary>
    ''' Insert a point at the given index
    ''' </summary>
    ''' <param name="index"></param>
    ''' <param name="value"></param>
    ''' <remarks>
    ''' </remarks>
    Public Shadows Sub Insert(ByVal index As Integer, ByVal value As PointF)
      MyBase.Insert(index, value)
      OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemAdded, value, index))
    End Sub

    ''' <summary>
    ''' Insert a series of points at the given index
    ''' </summary>
    ''' <param name="index"></param>
    ''' <param name="value"></param>
    ''' <remarks>
    ''' </remarks>
    Public Shadows Sub InsertRange(ByVal index As Integer, ByVal value As Generic.IEnumerable(Of PointF))
      MyBase.InsertRange(index, value)
      OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsAdded, value, index))
    End Sub

    ''' <summary>
    ''' Remove a point at the given index
    ''' </summary>
    ''' <param name="index"></param>
    ''' <remarks>
    ''' </remarks>
    Public Shadows Sub RemoveAt(ByVal index As Integer)
      Dim item As Object = MyBase.Item(index)
      If Me._selectedPoint = index Then Me._selectedPoint = NoPointSelected
      MyBase.RemoveAt(index)
      OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemDeleted, item, index))
    End Sub

    ''' <summary>
    ''' Remove the first point of the collection that matches the given point
    ''' </summary>
    ''' <param name="value"></param>
    ''' <remarks>
    ''' </remarks>
    Public Shadows Sub Remove(ByVal value As PointF)
      Me.RemoveAt(MyBase.IndexOf(value))
    End Sub

    ''' <summary>
    ''' Clear the collection of points
    ''' </summary>
    ''' <remarks>
    ''' </remarks>
    Public Shadows Sub Clear()
      MyBase.Clear()
      Me._selectedPoint = NoPointSelected
      OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsDeleted))
    End Sub

    ''' <summary>
    ''' Add a rectangle, i.e. 4 points, to the points.
    ''' </summary>
    ''' <param name="objRect"></param>
    ''' <remarks>This clears the points first
    ''' </remarks>
    Public Sub SetToRectangle(ByVal objRect As RectangleF)
      MyBase.Clear()

      Dim pt(4) As PointF
      pt(0).X = objRect.X
      pt(0).Y = objRect.Y
      pt(1).X = pt(0).X + objRect.Width
      pt(1).Y = pt(0).Y
      pt(2).X = pt(1).X
      pt(2).Y = pt(1).Y + objRect.Height
      pt(3).X = pt(2).X - objRect.Width
      pt(3).Y = pt(2).Y
      pt(4).X = pt(0).X
      pt(4).Y = pt(0).Y
      MyBase.AddRange(pt)
      OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
    End Sub

    ''' <summary>
    ''' Add a rectangle, i.e. 4 points, to the points using the top left and bottom right points
    ''' </summary>
    ''' <param name="pt1"></param>
    ''' <param name="pt2"></param>
    ''' <remarks></remarks>
    Public Sub SetToRectangle(ByVal pt1 As PointF, ByVal pt2 As PointF)
      SetToRectangle(New RectangleF(pt1, New SizeF(pt2.X - pt1.X, pt2.Y - pt1.Y)))
    End Sub

    ''' <summary>
    ''' Create a Annotation representing a circle
    ''' </summary>
    ''' <param name="center"></param>
    ''' <param name="radius"></param>
    ''' <param name="nrPts"></param>
    ''' <remarks></remarks>
    Public Sub SetToCircle(ByVal center As PointF, ByVal radius As Double, ByVal nrPts As Integer)
      SetToEllipse(center, radius, radius, 0, nrPts)
    End Sub

    ''' <summary>
    ''' Create a Annotation representing an ellipse. 
    ''' </summary>
    ''' <param name="center"></param>
    ''' <param name="semiMajorAxis"></param>
    ''' <param name="semiMinorAxis"></param>
    ''' <param name="angle">The angle of the major axis with the X-axis</param>
    ''' <param name="nrPts"></param>
    ''' <remarks>
    ''' </remarks>
    Public Sub SetToEllipse(ByVal center As PointF, ByVal semiMajorAxis As Double, ByVal semiMinorAxis As Double, _
                       ByVal angle As Double, ByVal nrPts As Integer)
      Dim angleStep As Double = Math.PI * 2 / nrPts, i As Integer
      Dim oPt(nrPts) As PointF
      MyBase.Clear()

      If angle = 0 Then
        For i = 0 To nrPts - 1
          Dim cosT, sinT As Double
          cosT = Math.Cos(angleStep * i)
          sinT = Math.Sin(angleStep * i)
          oPt(i).X = CSng(center.X + semiMajorAxis * cosT)
          oPt(i).Y = CSng(center.Y + semiMinorAxis * sinT)
        Next
      Else
        Dim cosAngle, sinAngle As Double
        angle = angle / 180 * Math.PI
        cosAngle = Math.Cos(angle)
        sinAngle = Math.Sin(angle)
        For i = 0 To nrPts - 1
          Dim cosT, sinT As Double
          cosT = Math.Cos(angleStep * i)
          sinT = Math.Sin(angleStep * i)
          oPt(i).X = CSng(center.X + semiMajorAxis * cosT * cosAngle - semiMinorAxis * sinT * sinAngle)
          oPt(i).Y = CSng(center.Y + semiMajorAxis * cosT * sinAngle + semiMinorAxis * sinT * cosAngle)
        Next
      End If

      oPt(nrPts).X = oPt(0).X
      oPt(nrPts).Y = oPt(0).Y

      MyBase.AddRange(oPt)
      OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
    End Sub

    ''' <summary>
    ''' Quantize an Annotation by removing neighbouring points that are too close to each other, i.e. closer than minDistance. 
    ''' Also add a point if they are too far from each other, i.e. further than maxDistance. 
    ''' Use 0 if one of the two distances is not needed.
    ''' </summary>
    ''' <param name="minDistance"></param>
    ''' <param name="maxDistance"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Quantize(ByVal minDistance As Double, ByVal maxDistance As Double) As Boolean
      Return Quantize(minDistance, maxDistance, 1)
    End Function

    ''' <summary>
    ''' Quantize an Annotation by removing points in range that are too close to each other, i.e. closer than minDistance. 
    ''' Also add a point if they are too far from each other, i.e. further than maxDistance. 
    ''' Use 0 if one of the two distances is not needed.
    ''' </summary>
    ''' <param name="minDistance"></param>
    ''' <param name="maxDistance"></param>
    ''' <param name="range"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Quantize(ByVal minDistance As Double, ByVal maxDistance As Double, ByVal range As Integer) As Boolean
      Dim i, j As Integer, distance As Double, pt1, pt2 As PointF, changed As Boolean = False

      'Remove last point if closed, we readd it later if required
      Dim count As Integer = MyBase.Count
      Debug.WriteLine("Quantize with " & count & " points")
      If count > 2 Then
        Dim pts As New Generic.List(Of PointF)
        pts.Add(MyBase.Item(0))

        For i = 1 To count - 1
          Dim tooClose As Boolean = False
          pt1 = MyBase.Item(i)
          For j = 1 To range
            If pts.Count - j < 0 Then
              pt2 = MyBase.Item(count + pts.Count - j)
              Debug.WriteLine("Current point index " & i & ", range " & j & " has index " & count + pts.Count - j)
            Else
              pt2 = pts.Item(pts.Count - j)
            End If

            distance = CIPF.Numerical.Algebra.Norm(pt1.X - pt2.X, pt1.Y - pt2.Y)

            If minDistance > 0 AndAlso distance < minDistance Then
              'we are too close and should not keep this point
              tooClose = True
              changed = True

              'If the range > 1 then we must remove intermediate points as they will form 'occlusions'
              If j > 1 And pts.Count - j >= 0 Then
                For k As Integer = 1 To j - 1
                  pts.RemoveAt(pts.Count - k)
                Next
              End If

              Exit For
            End If
          Next

          If Not tooClose Then
            pts.Add(pt1)
          End If
        Next

        'Check first point again seperately
        range = Math.Min(range, pts.Count)
        For j = 1 To range
          distance = CIPF.Numerical.Algebra.Norm(pts(0).X - pts(pts.Count - j).X, pts(0).Y - pts(pts.Count - j).Y)
          If minDistance > 0 AndAlso distance < minDistance Then
            For k As Integer = 1 To j
              pts.RemoveAt(pts.Count - k)
            Next
            changed = True
          End If
        Next

        'Now check for maximum distance!
        Dim pts2 As New Generic.List(Of PointF)
        count = pts.Count
        For i = 0 To count - 1
          pts2.Add(pts(i))

          If i > count - 2 Then
            j = 0
          Else
            j = i + 1
          End If

          distance = CIPF.Numerical.Algebra.Norm(pts(i).X - pts(j).X, pts(i).Y - pts(j).Y)
          If maxDistance > 0 AndAlso distance > maxDistance Then
            Dim nrPts As Integer = CInt(Math.Ceiling(distance / maxDistance))
            Dim dir As New PointF((pts(j).X - pts(i).X) / (nrPts + 1), (pts(j).Y - pts(i).Y) / (nrPts + 1))
            For k As Integer = 1 To nrPts
              pts2.Add(New PointF(pts(i).X + dir.X * k, pts(i).Y + dir.Y * k))
            Next
            changed = True
          End If
        Next

        'Add the points that were ok.
        MyBase.Clear()
        MyBase.AddRange(pts2)
      End If

      'Trigger event if collection has changed.
      If changed Then
        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
      End If
      Return changed
    End Function


    ''' <summary>
    ''' Move a all the points by the given amounts
    ''' </summary>
    ''' <param name="dx"></param>
    ''' <param name="dy"></param>
    ''' <remarks>This implementation is faster than using the .NET matrix class by a factor of 2-3.
    ''' </remarks>
    Public Overloads Sub Move(ByVal dx As Single, ByVal dy As Single)
      If Me.Count > 0 Then
        Debug.WriteLine("Moving by " & dx & "," & dy, "AnnotationPoints.Move")
        For i As Integer = 0 To MyBase.Count - 1
          Dim oldPt As PointF = MyBase.Item(i)
          MyBase.Item(i) = New PointF(oldPt.X + dx, oldPt.Y + dy)
        Next

        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
      End If
    End Sub

    ''' <summary>
    ''' Move a certain point by the given amounts
    ''' </summary>
    ''' <param name="index"></param>
    ''' <param name="dx"></param>
    ''' <param name="dy"></param>
    ''' <remarks>If the AnnotationPoints is closed and the point to be moved is the first or last, we move both points
    ''' </remarks>
    Public Overloads Sub Move(ByVal index As Integer, ByVal dx As Single, ByVal dy As Single)
      Debug.WriteLine("Moving point " & index & " by " & dx & "," & dy, "AnnotationPoints.Move")
      Dim oldPt As PointF = MyBase.Item(index)
      Dim pt As New PointF(oldPt.X + dx, oldPt.Y + dy)
      If index = Count - 1 Or index = 0 Then
        MyBase.Item(index) = pt
      Else
        MyBase.Item(index) = pt
      End If

      OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemSet, MyBase.Item(index), index))
    End Sub

    ''' <summary>
    ''' Expand a Annotation by a given absolute distance around a reference point. This is 
    ''' similar to scaling around a point, but in that case the amount a point is moved 
    ''' is proportional to its distance to the reference point. This transformation does NOT
    ''' retain the original shape!
    ''' </summary>
    ''' <param name="refPt"></param>
    ''' <param name="distance"></param>
    ''' <remarks>A negative value means a shrink</remarks>
    Public Overloads Sub ScaleRelative(ByVal refPt As PointF, ByVal distance As Single)
      If Me.Count > 0 Then
        For i As Integer = 0 To MyBase.Count - 1
          Dim oldPt As PointF = MyBase.Item(i)
          Dim dir As New PointF(oldPt.X - refPt.X, oldPt.Y - refPt.Y)
          Dim dirDistance As Double = Math.Sqrt(Math.Pow(dir.X, 2) + Math.Pow(dir.Y, 2))
          If dirDistance > 0 Then
            Dim scale As Double = (dirDistance + distance) / dirDistance
            MyBase.Item(i) = New PointF(refPt.X + scale * dir.X, oldPt.Y + scale * dir.Y)
          End If
        Next

        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
      End If
    End Sub

    ''' <summary>
    ''' Expand a number of times
    ''' </summary>
    ''' <param name="repeats"></param>
    ''' <remarks></remarks>
    Public Sub Expand(ByVal repeats As Integer)
      For i As Integer = 0 To repeats - 1
        Me.Expand(1, 2)
      Next
    End Sub

    ''' <summary>
    ''' Shrink a number of times.
    ''' </summary>
    ''' <param name="repeats"></param>
    ''' <remarks></remarks>
    Public Sub Shrink(ByVal repeats As Integer)
      For i As Integer = 0 To repeats - 1
        Me.Expand(-1, 2)
      Next
    End Sub

    ''' <summary>
    ''' Expand an annotation along the point angle bisectors by a given distance. A negative value means a shrink
    ''' This operation may modify the number of points of an annotation as it needs to quantize the annotation to a fine resolution.
    ''' It will also add and remove points if required.
    ''' </summary>
    ''' <param name="distance"></param>
    ''' <remarks>Use only distance 1 or -1 for general polygons. 
    ''' This routine is somewhat unstable if used with larger values, it is better to use it repeatedly with small values (see overloaded methods).</remarks>
    Public Sub Expand(ByVal distance As Single, ByVal quantization As Single)
      If Me.Count > 2 Then
        'Quantize first up to 1 point resolution.
        'Me.Quantize(quantization, quantization + 1, 1)
        Me.Quantize(quantization, 2 * quantization, 5)

        Dim bisector As PointF
        Dim nrPts As Integer = Me.Count
        Dim pts(nrPts - 1) As PointF
        Dim pt1, pt2, pt3, bsPt As PointF

        'We determine the sign of the interior using the given order of points.
        Dim interiorSign, temp As Double
        bisector = Numerical.Geometry.Bisector(Me.Item(0), Me.Item(1), Me.Item(2))
        bsPt.X = Me.Item(1).X + bisector.X
        bsPt.Y = Me.Item(1).Y + bisector.Y
        interiorSign = Numerical.Geometry.HalfSpace(bsPt, Me.Item(0), Me.Item(1))
        If Not Me.IsVisible(bsPt) Then interiorSign *= -1

        For index As Integer = 0 To nrPts - 1
          pt2 = Me.Item(index)
          If index >= nrPts - 1 Then
            pt3 = Me.Item(0)
          Else
            pt3 = Me.Item(index + 1)
          End If
          If index < 1 Then
            pt1 = Me.Item(nrPts - 1)
          Else
            pt1 = Me.Item(index - 1)
          End If

          bisector = Numerical.Geometry.Bisector(pt1, pt2, pt3)

          'Test whether the bisector is internal or external!
          bsPt.X = pt2.X + bisector.X
          bsPt.Y = pt2.Y + bisector.Y
          temp = Numerical.Geometry.HalfSpace(bsPt, pt1, pt2) * interiorSign * distance
          pts(index).X = pt2.X - temp * bisector.X
          pts(index).Y = pt2.Y - temp * bisector.Y
        Next

        MyBase.Clear()
        MyBase.AddRange(pts)
        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
      End If
    End Sub

    ''' <summary>
    ''' Scale the points.
    ''' </summary>
    ''' <param name="scale">The scaling factor.</param>
    ''' <remarks>
    ''' </remarks>
    Public Sub Scale(ByVal scale As Double)
      If Me.Count > 0 Then
        Debug.WriteLine("Scaling by " & scale & "AnnotationPoints.Scale")
        For i As Integer = 0 To MyBase.Count - 1
          Dim oldPt As PointF = MyBase.Item(i)
          MyBase.Item(i) = New PointF(CSng(oldPt.X * scale), CSng(oldPt.Y * scale))
        Next

        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
      End If
    End Sub

    ''' <summary>
    ''' Scale the points around a reference point
    ''' </summary>
    ''' <param name="scale">The scaling factor.</param>
    ''' <remarks>
    ''' </remarks>
    Public Sub Scale(ByVal scale As Double, ByVal refPt As PointF)
      If Me.Count > 0 Then
        Debug.WriteLine("Scaling by " & scale & " around point " & refPt.ToString(), "AnnotationPoints.Scale")
        For i As Integer = 0 To MyBase.Count - 1
          Dim oldPt As PointF = MyBase.Item(i)
          MyBase.Item(i) = New PointF(refPt.X + (oldPt.X - refPt.X) * scale, refPt.Y + (oldPt.Y - refPt.Y) * scale)
        Next

        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
      End If
    End Sub

    ''' <summary>
    ''' Flip around an axis.
    ''' </summary>
    ''' <param name="y"></param>
    ''' <remarks></remarks>
    Public Sub Flip(ByVal y As Double, ByVal axis As Integer)
      If Me.Count > 0 Then
        If axis > 0 Then
          For i As Integer = 0 To MyBase.Count - 1
            Dim oldPt As PointF = MyBase.Item(i)
            MyBase.Item(i) = New PointF(oldPt.X, CSng(2 * y - oldPt.Y))
          Next
        Else
          For i As Integer = 0 To MyBase.Count - 1
            Dim oldPt As PointF = MyBase.Item(i)
            MyBase.Item(i) = New PointF(CSng(2 * y - oldPt.X), oldPt.Y)
          Next
        End If

        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
      End If
    End Sub

    ''' <summary>
    ''' Rotate the points around a reference point
    ''' </summary>
    ''' <param name="angle"></param>
    ''' <param name="refPt"></param>
    ''' <remarks></remarks>
    Public Sub Rotate(ByVal angle As Double, ByVal refPt As PointF)
      Dim m As New System.Drawing.Drawing2D.Matrix
      m.Translate(-refPt.X, -refPt.Y)
      m.Rotate(angle, MatrixOrder.Append)
      m.Translate(refPt.X, refPt.Y, MatrixOrder.Append)
      Transform(m)
    End Sub

    ''' <summary>
    ''' Transform the Annotation points using the given transform. Use this for rotations and more complex transforms.
    ''' </summary>
    ''' <param name="m"></param>
    ''' <remarks>If one only needs to scale or move it is faster by a factor of 2-3 to use the 'Scale' and 'Move' methods.</remarks>
    Public Sub Transform(ByVal m As Matrix)
      If Me.Count > 0 Then
        Dim oPts() As PointF = Me.ToArray
        m.TransformPoints(oPts)
        MyBase.Clear()
        MyBase.AddRange(oPts)
        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
      End If
    End Sub

    ''' <summary>
    ''' Clip Annotation with a rectangle. The origin of the clip rectangle becomes the new origin!
    ''' </summary>
    ''' <param name="rect"></param>
    ''' <remarks></remarks>
    Public Sub Clip(ByVal rect As RectangleF)
      If Me.Count > 0 Then
        Dim newPts As New Generic.List(Of PointF)
        Dim translation As New SizeF(-rect.X, -rect.Y)
        Dim rectPts() As PointF = {rect.Location, New PointF(rect.Top, rect.Right), New PointF(rect.Bottom, rect.Right), New PointF(rect.Bottom, rect.Left), rect.Location}
        For i As Integer = 0 To Me.Count - 1
          If rect.Contains(Me.Item(i)) Then
            'Check if previous point lies outside.
            'Point is outside, check previous point: if also outside then eliminate
            If i > 0 Then
              If Not rect.Contains(Me.Item(i - 1)) Then
                'Add intersection first
                Dim intersectionPt As New PointF(0, 0)
                'Dim foundIntersection As Boolean = False
                For j As Integer = 0 To 3
                  If Numerical.Geometry.PolygonIntersection(Me.Item(i), Me.Item(i - 1), rectPts(j), rectPts(j + 1), intersectionPt) Then
                    Exit For
                  End If
                Next
                newPts.Add(PointF.Add(intersectionPt, translation))
              End If
            End If
            newPts.Add(PointF.Add(Me.Item(i), translation))
          Else
            'Point is outside, check previous point: if also outside then eliminate
            Dim previousPt As PointF
            If i = 0 Then
              previousPt = Me.Item(Me.Count - 1)
            Else
              previousPt = Me.Item(i - 1)
          End If

            If rect.Contains(previousPt) Then
              'Add intersection. Should always be present
              Dim intersectionPt As New PointF(0, 0)
              'Dim foundIntersection As Boolean = False
              For j As Integer = 0 To 3
                If Numerical.Geometry.PolygonIntersection(Me.Item(i), previousPt, rectPts(j), rectPts(j + 1), intersectionPt) Then
                  'foundIntersection = True
                  Exit For
                End If
              Next

              newPts.Add(PointF.Add(Me.Item(i), translation))
            Else
              'Eliminate current point, i.e. do nothing
            End If
          End If
        Next

        'Clear current collection and add new collection
        MyBase.Clear()
        Me.AddRange(newPts)
        OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.ItemsSet))
      End If
    End Sub

    '''' <summary>
    '''' Determine if a Annotation is closed, i.e. if the last and first point are the same
    '''' </summary>
    '''' <returns></returns>
    '''' <remarks>We needed this for some of the manipulation of the points collection, but there
    '''' is a copy in the Annotation class.
    '''' </remarks>
    'Public Function IsClosed() As Boolean
    '  If Count > 1 Then
    '    Return MyBase.Item(0).Equals(MyBase.Item(Count - 1))
    '  Else
    '    Return False
    '  End If
    'End Function

    ''' <summary>
    ''' Determine the bounding box of the Annotation points
    ''' </summary>
    ''' <returns>A <b>Rectangle</b> with the bounding box</returns>
    ''' <remarks>
    ''' </remarks>
    Public Overridable Function GetBoundingBox() As Rectangle
      If Count > 0 Then
        Dim i, iMaxY, iMinY, iMaxX, iMinX As Integer
        iMaxX = -Integer.MaxValue
        iMaxY = -Integer.MaxValue
        iMinX = Integer.MaxValue
        iMinY = Integer.MaxValue
        For i = 0 To Count - 1
          If MyBase.Item(i).X > iMaxX Then iMaxX = CInt(Math.Ceiling(MyBase.Item(i).X))
          If MyBase.Item(i).Y > iMaxY Then iMaxY = CInt(Math.Ceiling(MyBase.Item(i).Y))
          If MyBase.Item(i).X < iMinX Then iMinX = CInt(Math.Floor(MyBase.Item(i).X))
          If MyBase.Item(i).Y < iMinY Then iMinY = CInt(Math.Floor(MyBase.Item(i).Y))
        Next
        Return New Rectangle(iMinX, iMinY, iMaxX - iMinX, iMaxY - iMinY)
      Else
        Return System.Drawing.Rectangle.Empty
      End If
    End Function

    ''' <summary>
    ''' Determine the centroid.
    ''' </summary>
    ''' <returns>A <b>pointf</b> with the centroid</returns>
    ''' <remarks>
    ''' </remarks>
    Public Overridable Overloads Function GetCentroid() As PointF
      Return Me.GetCentroid(CSng(Me.GetArea))
    End Function

    ''' <summary>
    ''' Determine the centroid, with the area already precomputed
    ''' </summary>
    ''' <param name="sArea">The already computed area</param>
    ''' <returns>A <b>pointf</b> with the centroid</returns>
    ''' <remarks>
    ''' </remarks>
    Public Overridable Overloads Function GetCentroid(ByVal sArea As Single) As PointF
      Return Numerical.Geometry.PolygonCentroid(Me, sArea)
    End Function

    ''' <summary>
    ''' Determine the area taken up by the Annotation, in pixels units
    ''' </summary>
    ''' <returns>The area</returns>
    ''' <remarks>
    ''' </remarks>
    Public Overridable Function GetArea() As Double
      Return Numerical.Geometry.PolygonArea(Me)
    End Function

    ''' <summary>
    ''' Determine the border length, in pixel units.
    ''' </summary>
    ''' <returns>The length</returns>
    ''' <remarks>
    ''' </remarks>
    Public Overridable Function GetPerimeter() As Double
      Return Numerical.Geometry.PolygonPerimeter(Me)
    End Function

    ''' <summary>
    ''' Return the shape compactness parameter, a value between 0 - 1. A circle has 1, a square 0.9. 
    ''' A rectangle with one dimension twice the other has compactness 0.83.
    ''' Use this version when the area and perimeter have already been computed
    ''' </summary>
    ''' <param name="perimeter"></param>
    ''' <param name="area"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overridable Function GetCompactness(ByVal perimeter As Double, ByVal area As Double) As Double
      Return Numerical.Geometry.PolygonCompactness(Me, perimeter, area)
    End Function

    ''' <summary>
    ''' Return the shape compactness parameter, a value between 0 - 1. A circle has 1, a square 0.9. 
    ''' A rectangle with one dimension twice the other has compactness 0.83.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overridable Function GetCompactness() As Double
      Return Numerical.Geometry.PolygonCompactness(Me)
    End Function

    ''' <summary>
    ''' String representation of the Annotation
    ''' </summary>
    ''' <param name="strCoordinateSep"></param>
    ''' <param name="strPointSeperator"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' </remarks>
    Public Shadows Function ToString(ByVal strCoordinateSep As String, ByVal strPointSeperator As String) As String
      Dim i As Integer, strText As String = ""
      For i = 0 To Count - 1
        If i > 0 Then strText = strText & strPointSeperator
        strText = strText & MyBase.Item(i).X & strCoordinateSep & MyBase.Item(i).Y
      Next
      Return strText
    End Function
#End Region

#Region "Draw and related methods"

    ''' <summary>
    ''' Create a graphics path from this Annotation. Used when drawing and performing hit tests.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>
    ''' </remarks>
    Private Function GetGraphicsPath() As GraphicsPath
      If Count > 1 Then
        Dim oGP As New GraphicsPath
        If Me._closed And Me.Count > 2 Then
          oGP.AddPolygon(ToArray())
        Else
          oGP.AddLines(ToArray())
        End If
        Return oGP
      Else
        Return Nothing
      End If
    End Function

    ''' <summary>
    ''' Return a copy of the region corresponding to this Annotation.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>Calling this does not turn on internal caching of the graphicspath or region, 
    ''' so the region is recreated on every call! We might win a little time by checking the 
    ''' caching and just cloning the internal region if this is on, but it's probably hardly worth it.
    ''' </remarks>
    Public Function GetRegion() As Region
      If Me.Count > 1 Then
        Return New Region(Me.GetGraphicsPath())
      Else
        Return Nothing
      End If
    End Function

    ''' <summary>
    ''' Trivial
    ''' </summary>
    ''' <remarks>
    ''' </remarks>
    Private Sub UpdateCachedRegion()
      If Me.needRegionUpdate Then
        'Debug.WriteLine("Updating cached region for Annotation " & Me.Key)
        _region = Util.GraphicsPathToRegion(Me.Path)
        Me.needRegionUpdate = False
      End If
    End Sub

    ''' <summary>
    '''  Trivial
    ''' </summary>
    ''' <remarks>
    ''' </remarks>
    Private Sub UpdateCachedPath()
      If Me.needPathUpdate Then
        'Debug.WriteLine("Updating cached path for Annotation " & Me.Key)
        _path = Me.GetGraphicsPath
        Me.needPathUpdate = False
      End If
    End Sub

    ''' <summary>
    ''' Determine if a point is inside the area enclosed by the Annotation.
    ''' </summary>
    ''' <param name="pt">The point to be tested.</param>
    ''' <returns></returns>
    ''' <remarks>Calling this method will cause caching of the internal region, 
    ''' thereby speeding up any subsequent calls.
    ''' </remarks>
    Public Function IsVisible(ByVal pt As Point) As Boolean
      Dim rgn As Region = Me.Region
      If rgn Is Nothing Then
        Return False
      Else
        Return rgn.IsVisible(pt)
      End If
    End Function

    ''' <summary>
    ''' Determine if a point is inside the area enclosed by the Annotation.
    ''' </summary>
    ''' <param name="x"></param>
    ''' <param name="y"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function IsVisible(ByVal x As Integer, ByVal y As Integer) As Boolean
      Dim rgn As Region = Me.Region
      If rgn Is Nothing Then
        Return False
      Else
        Return rgn.IsVisible(x, y)
      End If
    End Function

    ''' <summary>
    ''' Determine if a point is inside the area enclosed by the Annotation. 
    ''' </summary>
    ''' <param name="pt "></param>
    ''' <returns></returns>
    ''' <remarks>IMPORTANT: Do not use the GraphicsPath.IsVisible method in .NET, 
    ''' it is buggy and does not work for floating point coordinates!</remarks>
    Public Function IsVisible(ByVal pt As PointF) As Boolean
      Return Numerical.Geometry.PointInPolygon(pt, Me)
    End Function


    ''' <summary>
    ''' Determine if a point is on the path of an Annotation when drawn.
    ''' </summary>
    ''' <param name="pt"></param>
    ''' <returns></returns>
    ''' <remarks>This will always return false if the Annotation has not been drawn before!
    ''' </remarks>
    Protected Friend Function IsOutlineVisible(ByVal pt As PointF, ByVal info As DrawingInfo) As Boolean
      If Me.Count > 1 Then
        Dim oP As Pen = info.GetSelectionPen(Me._lineWidth)
        Return Path.IsOutlineVisible(pt, oP)
        oP.Dispose()
      ElseIf Me.Count = 1 Then
        If Me.IsPointVisible(pt, info) > -1 Then
          Return True
        Else
          Return False
        End If
      End If
    End Function

    ''' <summary>
    ''' Determine if a point is on a marker of point when drawn, and more precisely which point
    ''' </summary>
    ''' <param name="pt"></param>
    ''' <returns>The index of the corresponding point, else -1. This will always return false if the Annotation has not been drawn before!</returns>
    ''' <remarks>
    ''' </remarks>
    Protected Friend Function IsPointVisible(ByVal pt As PointF, ByVal info As DrawingInfo) As Integer
      Dim drawingPointRectangleSize As Single = info.GetDrawingPointRectangleSize(Me._pointSize) + 2
      Dim pointHalfWidth As Single = CSng(drawingPointRectangleSize / 2)
      Dim rect As New RectangleF(pt.X - pointHalfWidth, pt.Y - pointHalfWidth, drawingPointRectangleSize, drawingPointRectangleSize)
      For i As Integer = 0 To Me.Count - 1
        If rect.Contains(Me.Point(i)) Then
          Return i
        End If
      Next
      Return NoPointSelected
    End Function

    ''' <summary>
    ''' The main drawing routine.
    ''' </summary>
    ''' <param name="info"></param>
    ''' <param name="parentSelected "></param>
    ''' <param name="parentText"></param>
    ''' <remarks>The scaling and offset are already set in the graphics object using a transformation. 
    ''' This is easy for the Annotation point locations, but inconvenient for the line and text dimensions.
    ''' These need to be scaled properly depending on the drawing unit which takes the dimension 
    ''' mode into account (screen or image).
    ''' The zoom is only used to determine pen and text size. !!!</remarks>
    Friend Sub Draw(ByVal g As Graphics, ByVal info As DrawingInfo, _
                    ByVal parentSelected As Boolean, _
                    ByVal parentText As String)
      Debug.WriteLine("Drawing Annotation " & Me.Key, "Annotation.Draw")
      If (Me.Visible = True) And Me.Count > 0 Then
        'Get the cached graphics _path
        Dim pathLines As GraphicsPath = Me.Path

        'Set the line and point sizes.
        Dim drawingLineWidth, drawingPointRectangleSize As Single
        drawingPointRectangleSize = info.GetDrawingPointRectangleSize(Me._pointSize)
        drawingLineWidth = info.GetDrawingLineWidth(Me._lineWidth)
        Dim fontDrawSize As Single = info.GetDrawingFontSize(Me._fontSize)

        'Get pens and brushes
        Dim p1 As Pen = info.GetPen(drawingLineWidth, Me._color)
        Dim p2 As Pen = info.GetSelectedPen1(drawingLineWidth)
        Dim b1 As New SolidBrush(_color)
        Dim b2 As New SolidBrush(Drawing.Color.White)

        Dim drawText As String

        'First point is best obtained directly from the points collection because
        'the graphicspath may still be nothing when there is only 1 point (no line to draw!)
        Dim textPt As PointF
        If Me.Count = 1 Then
          textPt = New PointF(Me.Point(0).X, Me.Point(0).Y)
        Else
          textPt = New PointF(pathLines.PathPoints(0).X, pathLines.PathPoints(0).Y)
        End If

        If Not Me.Selected And Not parentSelected Or _
          Me.Selected And Me.SelectedPoint <> NoPointSelected Then
          'UNSELECTED, or just a POINT selected

          'The lines
          If _lineWidth > 0 And Me.Count > 1 Then g.DrawPath(p1, pathLines)

          'The point markers.
          If Me._pointSize > 0 Then
            If Me.Count > 1 Then
              g.FillRectangles(b1, Util.PointToRectangle(pathLines.PathPoints, drawingPointRectangleSize))
            Else
              g.FillRectangle(b1, Util.PointToRectangle(textPt, drawingPointRectangleSize))
            End If

            'Draw selected point if any
            If Me.SelectedPoint <> NoPointSelected AndAlso Me.SelectedPoint < Me.Count Then
              Dim rect As RectangleF = Util.PointToRectangle(Me.Point(Me.SelectedPoint), drawingPointRectangleSize)
              g.FillRectangle(b2, rect)
              g.DrawRectangle(p2, rect.X, rect.Y, rect.Width, rect.Height)
            End If
          End If

          'The text uses the same brush
          drawText = GetText(parentText)
          If Not drawText Is Nothing Then
            Dim drawFont As New Font(FontFamily.GenericSansSerif, fontDrawSize, FontStyle.Bold)
            g.DrawString(drawText, drawFont, b1, textPt.X + TextOffset, textPt.Y + TextOffset)
            drawFont.Dispose()
          End If
        Else
          'SELECTED
          'We use an white interior with a black border for text and points
          'Dashed line is good for the lines, but it is more efficient to widen 
          'the path extended with rectangle and text in one go

          'The lines, and black and white dashed
          If _lineWidth > 0 And Me.Count > 1 Then
            Dim p3 As Pen = info.GetSelectedPen2(drawingLineWidth)
            g.DrawPath(p2, pathLines)

            g.DrawPath(p3, pathLines)
            p3.Dispose()
          End If

          'The point markers.
          Dim pathMarkersAndText As New GraphicsPath
          pathMarkersAndText.AddRectangles(Util.PointToRectangle(Me, drawingPointRectangleSize))

          'The text
          drawText = GetText(parentText)
          If Not drawText Is Nothing Then
            pathMarkersAndText.AddString(drawText, FontFamily.GenericSansSerif, FontStyle.Bold, fontDrawSize, _
              New PointF(textPt.X + TextOffset, textPt.Y + TextOffset), StringFormat.GenericDefault)
          End If

          'Reset p2 to solid white
          Dim p4 As Pen = info.GetPen(1, Drawing.Color.Black)
          g.DrawPath(p4, pathMarkersAndText)
          g.FillPath(b2, pathMarkersAndText)

          pathMarkersAndText.Dispose()
          p4.Dispose()
        End If

        'Ceanup
        b1.Dispose()
        b2.Dispose()
        p1.Dispose()
        p2.Dispose()
      End If
    End Sub

    Private Function GetText(ByVal parentText As String) As String
      If Not _text Is Nothing Then
        If _text.Length > 0 Then
          If Not parentText Is Nothing Then
            If parentText.Length > 0 Then
              Return parentText & ":" & _text
            Else
              Return _text
            End If
          Else
            Return _text
          End If
        Else
          If Not parentText Is Nothing Then
            If parentText.Length > 0 Then
              Return parentText
            End If
          End If
        End If
      Else
        If Not parentText Is Nothing Then
          If parentText.Length > 0 Then
            Return parentText
          End If
        End If
      End If
      Return Nothing
    End Function
#End Region

  End Class
End Namespace
